package sweezy.swf.utils
{
	import sweezy.swf.SWF;

	public class SWFVerifier
	{

		public function SWFVerifier()
		{
		}

		public function verify(swf:SWF):Array
		{
			var errors:Array = [];
			new SWFWalker(errors).visitSWF(swf);
			return errors;
		}
	}
}

import sweezy.core.utils.Messages;
import sweezy.swf.IElement;
import sweezy.swf.ITag;
import sweezy.swf.SWF;
import sweezy.swf.action.IAction;
import sweezy.swf.constant.AlphaBitmapType;
import sweezy.swf.constant.BitmapType;
import sweezy.swf.constant.CompressType;
import sweezy.swf.constant.SWFVersion;
import sweezy.swf.structure.ABCData;
import sweezy.swf.structure.ActionRecords;
import sweezy.swf.structure.AlphaBitmapData;
import sweezy.swf.structure.AlphaColorMapData;
import sweezy.swf.structure.Asset;
import sweezy.swf.structure.BevelFilter;
import sweezy.swf.structure.BitmapData15;
import sweezy.swf.structure.BitmapData24;
import sweezy.swf.structure.BlurFilter;
import sweezy.swf.structure.ButtonCondAction;
import sweezy.swf.structure.ButtonRecord;
import sweezy.swf.structure.ClipActionRecord;
import sweezy.swf.structure.ClipActions;
import sweezy.swf.structure.ColorMapData;
import sweezy.swf.structure.ColorMatrixFilter;
import sweezy.swf.structure.ConvolutionFilter;
import sweezy.swf.structure.FocalGradient;
import sweezy.swf.structure.Header;
import sweezy.swf.structure.IFilter;
import sweezy.swf.structure.RGB;
import sweezy.swf.structure.RGBA;
import sweezy.swf.tag.CSMTextSettings;
import sweezy.swf.tag.DebugID;
import sweezy.swf.tag.DefineBinaryData;
import sweezy.swf.tag.DefineBits;
import sweezy.swf.tag.DefineBitsJPEG2;
import sweezy.swf.tag.DefineBitsJPEG3;
import sweezy.swf.tag.DefineBitsJPEG4;
import sweezy.swf.tag.DefineBitsLossless;
import sweezy.swf.tag.DefineBitsLossless2;
import sweezy.swf.tag.DefineButton;
import sweezy.swf.tag.DefineButton2;
import sweezy.swf.tag.DefineButtonCxform;
import sweezy.swf.tag.DefineButtonSound;
import sweezy.swf.tag.DefineEditText;
import sweezy.swf.tag.DefineFont;
import sweezy.swf.tag.DefineFont2;
import sweezy.swf.tag.DefineFont3;
import sweezy.swf.tag.DefineFont4;
import sweezy.swf.tag.DefineFontAlignZones;
import sweezy.swf.tag.DefineFontInfo;
import sweezy.swf.tag.DefineFontInfo2;
import sweezy.swf.tag.DefineFontName;
import sweezy.swf.tag.DefineMorphShape;
import sweezy.swf.tag.DefineMorphShape2;
import sweezy.swf.tag.DefineScalingGrid;
import sweezy.swf.tag.DefineSceneAndFrameLabelData;
import sweezy.swf.tag.DefineShape;
import sweezy.swf.tag.DefineShape2;
import sweezy.swf.tag.DefineShape3;
import sweezy.swf.tag.DefineShape4;
import sweezy.swf.tag.DefineSound;
import sweezy.swf.tag.DefineSprite;
import sweezy.swf.tag.DefineText;
import sweezy.swf.tag.DefineText2;
import sweezy.swf.tag.DefineVideoStream;
import sweezy.swf.tag.EnableDebugger;
import sweezy.swf.tag.EnableDebugger2;
import sweezy.swf.tag.ExportAssets;
import sweezy.swf.tag.FileAttributes;
import sweezy.swf.tag.FrameLabel;
import sweezy.swf.tag.Metadata;
import sweezy.swf.utils.VerifierMessages;
import sweezy.swf.visitor.SWFElementVisitor;

class SWFWalker extends SWFElementVisitor
{

	private static const FIXED8_MAX_VALUE:Number = 32767 / 65536;

	private static const FIXED8_MIN_VALUE:Number = -32768 / 65536;

	private static const FIXED_MAX_VALUE:Number = int.MAX_VALUE / 65536;

	private static const FIXED_MIN_VALUE:Number = int.MIN_VALUE / 65536;

	private static var _msgs:Messages = Messages.getInstance(VerifierMessages);

	private var _characters:Object;

	private var _errors:Array;

	private var _fileAttributes:FileAttributes;

	private var _header:Header;

	private var _tagIndex:uint;

	private var _version:uint;

	public function SWFWalker(errors:Array):void
	{
		_errors = errors;
		_characters = {};
	}

	override public function visitABCData(element:ABCData):void
	{
		checkNotNull(element, "data");
	}

	override public function visitActionRecords(element:ActionRecords):void
	{
		if (!checkArray(element, "records", true))
		{
			return;
		}

		var index:uint = 0;
		for each (var action:IAction in element.records)
		{
			if (action.version > _version)
			{
				error("010", index, action.actionName, action.version, _version);
			}

			index++;
		}
	}

	override public function visitAlphaBitmapData(element:AlphaBitmapData):void
	{
		checkNotNull(element, "bitmapPixelData");
		//TODO レングスチェック
	}

	override public function visitAlphaColorMapData(element:AlphaColorMapData):void
	{
		checkNotNull(element, "colormapPixelData");
		if (!checkArray(element, "colorTableRGBA", true))
		{
			return;
		}

		for each (var color:RGBA in element.colorTableRGBA)
		{
			color.accept(this);
		}
		//TODO レングスチェック
	}

	override public function visitBevelFilter(element:BevelFilter):void
	{
		if (checkNotNull(element, "highlightColor"))
		{
			element.highlightColor.accept(this);
		}
		if (checkNotNull(element, "shadowColor"))
		{
			element.shadowColor.accept(this);
		}

		checkFIXED(element, "blurX");
		checkFIXED(element, "blurY");
		checkFIXED(element, "angle");
		checkFIXED(element, "distance");
		checkFIXED8(element, "strength");
		checkNumber(element, "passes", 0, 15);
	}

	override public function visitBitmapData15(element:BitmapData15):void
	{
		checkNotNull(element, "bitmapPixelData");
		//TODO レングスチェック
	}

	override public function visitBitmapData24(element:BitmapData24):void
	{
		checkNotNull(element, "bitmapPixelData");
		//TODO レングスチェック
	}

	override public function visitBlurFilter(element:BlurFilter):void
	{
		checkFIXED(element, "blurX");
		checkFIXED(element, "blurY");
		checkNumber(element, "passes", 0, 31);
	}

	override public function visitButtonCondAction(element:ButtonCondAction):void
	{
		if (checkNotNull(element, "actions"))
		{
			element.actions.accept(this);
		}
	}

	override public function visitButtonRecord(element:ButtonRecord):void
	{
		if (element.buttonHasBlendMode && checkVersion(element, "buttonHasBlendMode", SWFVersion.VERSION_8))
		{
			if (element.defineButton2)
			{
				checkNumber(element, "blendMode", 0, 14);
			}
			else
			{
				error("200", "blendMode");
			}
		}
		if (element.buttonHasFilterList && checkVersion(element, "buttonHasFilterList", SWFVersion.VERSION_8))
		{
			if (element.defineButton2)
			{
				if (checkArray(element, "filterList", true))
				{
					for each (var filter:IFilter in element.filterList)
					{
						filter.accept(this);
					}
				}
			}
			else
			{
				error("200", "filterList");
			}
		}

		checkHasCharacter(element, "characterId");
		checkNotNull(element, "placeMatrix");

		if (element.defineButton2)
		{
			checkNotNull(element, "colorTransform");
		}
		else if (element.colorTransform !== null)
		{
			error("200", "colorTransform");
		}
	}

	override public function visitCSMTextSettings(element:CSMTextSettings):void
	{
		checkNumber(element, "gridFit", 0, 2);
		checkNumber(element, "useFlashType", 0, 1);
		if (checkHasCharacter(element, "textId"))
		{
			var character:ITag = _characters[element.textId];
			if (!(character is DefineText) && !(character is DefineText2) && !(character is DefineEditText))
			{
				error("203", character.elementName);
			}
		}
	}

	override public function visitClipActionRecord(element:ClipActionRecord):void
	{
		if (checkNotNull(element, "actions"))
		{
			element.actions.accept(this);
		}
		checkNotNull(element, "eventFlags");
	}

	override public function visitClipActions(element:ClipActions):void
	{
		if (checkArray(element, "clipActionRecords", true))
		{
			for each (var record:ClipActionRecord in element.clipActionRecords)
			{
				record.accept(this);
			}
		}
		checkNotNull(element, "allEventFlags");
	}

	override public function visitColorMapData(element:ColorMapData):void
	{
		checkNotNull(element, "colormapPixelData");
		if (!checkArray(element, "colorTableRGB", true))
		{
			return;
		}
		for each (var color:RGB in element.colorTableRGB)
		{
			color.accept(this);
		}
	}

	override public function visitColorMatrixFilter(element:ColorMatrixFilter):void
	{
		if (element.matrix === null || element.matrix.length !== 20)
		{
			error("201");
		}
	}

	override public function visitConvolutionFilter(element:ConvolutionFilter):void
	{
		if (element.matrix === null || element.matrix.length !== element.matrixX * element.matrixY)
		{
			error("202");
		}
		checkNotNull(element, "defaultColor");
	}

	override public function visitDebugID(element:DebugID):void
	{
		checkNotNull(element, "uuid");
	}

	override public function visitDefineBinaryData(element:DefineBinaryData):void
	{
		addCharacter(element, "tag");
		checkNotNull(element, "data");
	}

	override public function visitDefineBits(element:DefineBits):void
	{
		addCharacter(element, "characterId");
		checkNotNull(element, "jpegData");
	}

	override public function visitDefineBitsJPEG2(element:DefineBitsJPEG2):void
	{
		addCharacter(element, "characterId");
		checkNotNull(element, "imageData");
	}

	override public function visitDefineBitsJPEG3(element:DefineBitsJPEG3):void
	{
		addCharacter(element, "characterId");
		checkNotNull(element, "bitmapAlphaData");
		checkNotNull(element, "imageData");
	}

	override public function visitDefineBitsJPEG4(element:DefineBitsJPEG4):void
	{
		addCharacter(element, "characterId");
		checkNotNull(element, "bitmapAlphaData");
		checkNotNull(element, "imageData");
	}

	override public function visitDefineBitsLossless(element:DefineBitsLossless):void
	{
		addCharacter(element, "characterId");
		if (checkNumber(element, "format", 3, 5))
		{
			if (checkNotNull(element, "zlibBitmapData"))
			{
				switch (element.format)
				{
					case BitmapType.COLOR_MAP_DATA:
						//TODO レングスチェック
						if (!(element.zlibBitmapData is ColorMapData))
						{
							error("204");
						}
						return;
					case BitmapType.BITMAP_DATA_PIX_15:
						//TODO レングスチェック
						if (!(element.zlibBitmapData is BitmapData15))
						{
							error("204");
						}
						return;
					case BitmapType.BITMAP_DATA_PIX_24:
						//TODO レングスチェック
						if (!(element.zlibBitmapData is BitmapData24))
						{
							error("204");
						}
						break;
				}
				element.zlibBitmapData.accept(this);
			}
		}
	}

	override public function visitDefineBitsLossless2(element:DefineBitsLossless2):void
	{
		addCharacter(element, "characterId");
		if (checkNumber(element, "format", 3, 5))
		{
			if (checkNotNull(element, "zlibBitmapData"))
			{
				switch (element.format)
				{
					case AlphaBitmapType.ALPHA_COLOR_MAP_DATA:
						//TODO レングスチェック
						if (!(element.zlibBitmapData is AlphaColorMapData))
						{
							error("205");
						}
						return;
					case AlphaBitmapType.ALPHA_BITMAP_DATA_4:
					//passthrough
					case AlphaBitmapType.ALPHA_BITMAP_DATA_5:
						//TODO レングスチェック
						if (!(element.zlibBitmapData is AlphaBitmapData))
						{
							error("205");
						}
						break;
				}
				element.zlibBitmapData.accept(this);
			}
		}
	}

	override public function visitDefineButton(element:DefineButton):void
	{
		//TODO
	}

	override public function visitDefineButton2(element:DefineButton2):void
	{
		//TODO
	}

	override public function visitDefineButtonCxform(element:DefineButtonCxform):void
	{
		//TODO
	}

	override public function visitDefineButtonSound(element:DefineButtonSound):void
	{
		//TODO
	}

	override public function visitDefineEditText(element:DefineEditText):void
	{
		//TODO
	}

	override public function visitDefineFont(element:DefineFont):void
	{
		//TODO
	}

	override public function visitDefineFont2(element:DefineFont2):void
	{
		//TODO
	}

	override public function visitDefineFont3(element:DefineFont3):void
	{
		//TODO
	}

	override public function visitDefineFont4(element:DefineFont4):void
	{
		//TODO
	}

	override public function visitDefineFontAlignZones(element:DefineFontAlignZones):void
	{
		//TODO
	}

	override public function visitDefineFontInfo(element:DefineFontInfo):void
	{
		//TODO
	}

	override public function visitDefineFontInfo2(element:DefineFontInfo2):void
	{
		//TODO
	}

	override public function visitDefineFontName(element:DefineFontName):void
	{
		//TODO
	}

	override public function visitDefineMorphShape(element:DefineMorphShape):void
	{
		//TODO
	}

	override public function visitDefineMorphShape2(element:DefineMorphShape2):void
	{
		//TODO
	}

	override public function visitDefineScalingGrid(element:DefineScalingGrid):void
	{
		//TODO
	}

	override public function visitDefineSceneAndFrameLabelData(element:DefineSceneAndFrameLabelData):void
	{
		//TODO
	}

	override public function visitDefineShape(element:DefineShape):void
	{
		//TODO
	}

	override public function visitDefineShape2(element:DefineShape2):void
	{
		//TODO
	}

	override public function visitDefineShape3(element:DefineShape3):void
	{
		//TODO
	}

	override public function visitDefineShape4(element:DefineShape4):void
	{
		//TODO
	}

	override public function visitDefineSound(element:DefineSound):void
	{
		//TODO
	}

	override public function visitDefineSprite(element:DefineSprite):void
	{
		//TODO
	}

	override public function visitDefineText(element:DefineText):void
	{
		//TODO
	}

	override public function visitDefineText2(element:DefineText2):void
	{
		//TODO
	}

	override public function visitDefineVideoStream(element:DefineVideoStream):void
	{
		//TODO
	}

	override public function visitEnableDebugger(element:EnableDebugger):void
	{
		checkNotNull(element, "password");
	}

	override public function visitEnableDebugger2(element:EnableDebugger2):void
	{
		element.reserved
		checkNotNull(element, "password");
	}

	override public function visitExportAssets(element:ExportAssets):void
	{
		if (checkArray(element, "assets"))
		{
			for each (var asset:Asset in element.assets)
			{
				checkHasCharacter(asset, "tag");
			}
		}
	}

	override public function visitFocalGradient(element:FocalGradient):void
	{
		//TODO
	}

	override public function visitFrameLabel(element:FrameLabel):void
	{
		//TODO
	}

	override public function visitHeader(element:Header):void
	{
		//TODO LZMA
		if (element.compressType !== CompressType.ZLIB && element.compressType !== CompressType.NONE)
		{
			error("007");
		}
		if (element.compressed && element.compressType === CompressType.NONE)
		{
			error("008");
		}
		if (element.version < 1 || element.version > 13)
		{
			error("009");
		}
		if (element.frameSize === null)
		{
			error("100", "Header", "frameSize");
		}
	}

	override public function visitSWF(element:SWF):void
	{
		if (element === null)
		{
			error("001");
			return;
		}

		if (element.header === null)
		{
			error("002");
			return;
		}

		_header = element.header;
		_version = _header.version;
		element.header.accept(this);

		if (element.tags === null || element.tags.length === 0)
		{
			error("003");
			return;
		}

		var hasMetadata:Boolean = false;
		for each (var tag:ITag in element.tags)
		{
			if (tag === null)
			{
				error("004", _tagIndex);
				continue;
			}

			if (_version >= 8 && _tagIndex === 0 && !(tag is FileAttributes))
			{
				error("005");
			}
			if (tag.tagVersion > _version)
			{
				error("006", _tagIndex, tag.elementName, tag.tagVersion, _version);
			}

			if (tag is FileAttributes)
			{
				_fileAttributes = tag as FileAttributes;
			}
			if (tag is Metadata)
			{
				hasMetadata = true;
			}

			var numErrors:uint = _errors.length;
			tag.accept(this);
			if (_errors.length !== numErrors)
			{
				error("000", _tagIndex, tag.elementName);
			}

			_tagIndex++;
		}

		if (_fileAttributes !== null && _fileAttributes.hasMetadata !== hasMetadata)
		{
			error("011");
		}
	}

	private function addCharacter(element:IElement, property:String):Boolean
	{
		var id:uint = element[property];
		if (id in _characters)
		{
			error("107", element.elementName, property);
			return false;
		}

		_characters[id] = element;
		return true;
	}

	private function checkArray(element:IElement, property:String, verifyArrayElement:Boolean = false):Boolean
	{
		var target:Array = element[property] as Array;
		if (target === null || target.length === 0)
		{
			error("101", element.elementName, property);
			return false;
		}

		if (target is Array && verifyArrayElement)
		{
			var index:uint;
			for each (var item:* in target)
			{
				if (item === null)
				{
					error("104", element.elementName, property, index);
					return false;
				}
				index++;
			}
		}

		return true;
	}

	private function checkFIXED(element:IElement, property:String):Boolean
	{
		return checkNumber(element, property, FIXED_MIN_VALUE, FIXED_MAX_VALUE);
	}

	private function checkFIXED8(element:IElement, property:String):Boolean
	{
		return checkNumber(element, property, FIXED8_MIN_VALUE, FIXED8_MAX_VALUE);
	}

	private function checkFLOAT16(element:IElement, property:String):Boolean
	{
		var target:Number = element[property];
		if (isNaN(target) || !isFinite(target))
		{
			return true;
		}

		return checkNumber(element, property, -65504, 65504);
	}

	private function checkHasCharacter(element:IElement, property:String):Boolean
	{
		var id:uint = element[property];
		if (id in _characters)
		{

			return true;
		}

		error("108", element.elementName, property, id);
		return false;
	}

	private function checkNotNull(element:IElement, property:String):Boolean
	{
		var target:Object = element[property];
		if (target === null)
		{
			error("100", element.elementName, property);
			return false;
		}

		return true;
	}

	private function checkNumber(element:IElement, property:String, from:Number = Number.MIN_VALUE, to:Number = Number.MAX_VALUE):Boolean
	{
		var target:Number = element[property];
		if (isNaN(target))
		{
			error("102", element.elementName, property);
			return false;
		}

		if (from <= target && target <= to)
		{
			return true;
		}

		error("103", element.elementName, property, from, to, target);
		return false;
	}

	private function checkVersion(element:IElement, property:String, version:uint):Boolean
	{
		if (_version < version)
		{
			error("106", element.elementName, property, version, _version);
			return false;
		}

		return true;
	}

	private function error(key:String, ... args):void
	{
		_errors[_errors.length] = _msgs.format2(key, args);
	}
}
